Sliding Window 跟上篇 Multiple Pointers 類似,定義兩個指標,一個是 start,一個是 end。
像是:
| start | end | |||||
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 
start 與 end 根據題目所需來移動,這兩個指標所圍出的範圍可大可小,類似像可滑動的兩片窗戶一樣。
在追蹤陣列或字串的子集 (subset) 或子序列 (subsequence) 上非常實用。
舉一個最簡單的例子:
const array = [1, 2, 3, 4, 5]
let subarray = [1, 2, 3]
subarray = [2, 3]
subarray = [3, 4, 5]
subarray = [1]
Subarrays 必須要照原本陣列的元素排列順序與連續
const array = [1, 2, 3, 4, 5]
let subsequence = [1, 3, 5]
subsequence = [2, 4]
subsequence = [1, 2, 4, 5]
subsequence = [1]
Subsequence 只要照原本陣列的元素排列順序
const array = [1, 2, 3, 4, 5]
let subset = [5, 4, 1]
subset = [2, 3, 1]
subset = [5, 2, 3]
subset = [1]
Subsets 只要元素有包含在原本陣列即可,不用照順序與連續
給定一個數字陣列與正整數 n,請找出陣列中的最大子序列和,該子序列的長度必須等於 n。
這邊說要找子序列,所以是要相鄰的元素。
例如:[1, 3, 5, 7, 9, 2, 4, 6, 8, 10],n = 3,最大子序列 ([6, 8, 10]) 和為 24。
例如:[1],n = 2,回傳 null。
較差的解法:
function maxSubArraySum(arr, n){
    if (arr.length < n) return null;
    let maxSum = -Infinity;
    for(let i = 0; i < arr.length - n + 1; i++){
        let tempSum = 0;
        for(let j = 0; j < n; j++){
            tempSum += arr[i + j];
        }
        if(tempSum > maxSum){
            maxSum = tempSum;
        }
    }
    return maxSum;
}
以上有巢狀迴圈,所以在 arr.length 與 n 越來越大的情況下會跑 n * arr.length 次,時間複雜度會是 O(n²) 。
使用 Sliding Window 技巧:
function maxSubArraySum(arr, n) {
  if (arr.length < n) return null;
  let maxSum = 0;
  let tempSum = 0;
  for (let i = 0; i < n; i++) {
    maxSum += arr[i];
  }
  tempSum = maxSum;
  for (let i = n; i < arr.length; i++) {
    tempSum = tempSum - arr[i - n] + arr[i];
    maxSum = Math.max(maxSum, tempSum);
  }
  return maxSum;
  // 以範例 [1, 3, 5, 7, 9, 2, 4, 6, 8, 10],n = 3 為例:
  // 初始: [1, 3, 5]
  // 移到: [3, 5, 7]
  // 實際上只需減掉 1,與加上 7,不用每次都遞迴相加一次。
  // 換句話說就是每次減掉第一個元素,加上下一個元素。
  // 移到: [5, 7, 9]
  // 實際上只需減掉 3,與加上 9,不用每次都遞迴相加一次。
  // 以此類推
}
以上迴圈是平行的,在 arr.length 與 n 越來越大的情況下頂多跑 n + arr.length 次,時間複雜度會是 O(n) 。
給定一個字串,請找出其中最長且不重複字母的子字串。
0。例如:"abcabcbb",回傳 3,因為 "abc" 是最長的不重複字母的子字串。
例如:"bbbbb",回傳 1,因為 "b" 是最長的不重複字母的子字串。
以 'abccba' 為例:
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a | 
檢查目前 end 指到的字元 a 有沒有出現過,
沒有的話將目前 end 指到的字元 a 用 Frequency Counter 技巧記錄到物件內,紀錄 a 的 index ,
紀錄一下目前 start 到 end 範圍的長度
最後移動 end 到下一格。
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a | 
b 也沒出現過重複上述動作。
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a | 
c 也沒出現過重複上述動作。
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a | 
c 有出現過了,上次出現的位置是 2 ,
所以只好移動 start 到上次出現的位置的下一格,以排除之前出現過的字元。
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a | 
紀錄 c 的新出現過的位置 index 等於 3 ,
紀錄一下目前 start 到 end 範圍的長度
最後移動 end 到下一格。
| start | |||||
|---|---|---|---|---|---|
| end | |||||
| a | b | c | c | b | a | 
b 雖然有出現過,但上次出現的位置是 1 ,目前 start 的位置是 3 ,已經排除該字元了,所以不用變更 start 的位置。
紀錄 b 的新出現過的位置 index 等於 4 ,
紀錄一下目前 start 到 end 範圍的長度
最後移動 end 到下一格。
以此類推直到 end 移到陣列最後一個位置。
function findLongestSubstring(str) {
// 字串小於等於 1 個字元,直接回傳字串長度;
if (str.length <= 1) return str.length;
// 此解法也有用到 Frequency Counter 技巧來紀錄每個字元最後出現的位置。
let longest = 0;
let seen = {};
let start = 0;
for (let i = 0; i < str.length; i++) {
  let char = str[i];
  //  檢查有無出現過且出現過的位置比 start 還後面,則移動 start 到上次出現過的位置的後一格
  if (Object.prototype.hasOwnProperty.call(seen, char) && seen[char] >= start) {
      start = seen[char] + 1;
  }
  // 將目前的字串長度跟之前的最長字串長度比較,取最長的。
  longest = Math.max(longest, i - start + 1);
  // 更新目前字元出現的位置。
  seen[char] = i;
}
return longest;
}
小小建議"檢查有無出現過且出現過的位置比 start 還後面,則移動 start 到上次出現過的位置的後一格"這一段可以改一下,此系列文讓我受益良多,感謝~
if (Object.prototype.hasOwnProperty.call(seen, char) && seen[char] >= start) {
    start = seen[char] + 1;
}
感謝勘誤!